package communication ;

import java.io.BufferedReader ;
import java.io.IOException ;
import java.io.InputStreamReader ;
import java.net.MalformedURLException ;
import java.rmi.AlreadyBoundException ;
import java.rmi.Naming ;
import java.rmi.NotBoundException ;
import java.rmi.RemoteException ;
import java.rmi.registry.LocateRegistry ;

import persistence.Definitions ;
import persistence.PersistenceFactory ;
import persistence.hibernate.HibernatePersistenceFactory ;
import persistence.xstream.XStreamPersistenceFactory ;
import util.Bug ;
import util.Log ;
import util.Subsystem ;
import domain.ForumData ;
import domain.LocalForum ;
import domain.searchEngine.SearchEngineFactory ;
import domain.searchEngine.compass.CompassSearchEngineFactory ;
import domain.searchEngine.origin.OriginSearchEngineFactory ;

/**
 * a wrapper for Naming to cleanly set the forum.
 * @author idan
 */
public class Server
{
	private static class Config
	{
		public static enum PersistenceComponent { HIBERNATE, XSTREAM }
		public static enum SearchEngineComponent { COMPASS, ORIGIN }

		public boolean fresh ;
		public PersistenceComponent persistence ;
		public SearchEngineComponent searchEngine ;

		public Config()
		{
			// empty
		}
		@Override
		public String toString()
		{
			return this.getClass().getCanonicalName() + ": persistence = " + this.persistence +
				"; search engine = " + this.searchEngine + "; fresh = " + this.fresh ;
		}
	}

	private static Process dns ;
	private static final String DNS_EXECUTION_COMMAND = "rmiregistry" ;

	private static final int MAX_RETRIES = 3 ;

	private static final boolean START_DNS_EXTERNALLY = false ;

	private final String dnsAddress ;
	private final int dnsPort ;
	private final LocalForum forum ;
	private final String resourceName ;

	/**
	 * @param args command line options:
	 * -f				do not load from database, create a fresh one instead.
	 * -p=persistence	persistence component to use, should be "hibernate" or "xstream", case insensitive.
	 * -s=search		search engine to use, should be "origin" or "compass", case insensitive.
	 */
	public static void main(final String[] args)
	{
		Server server = null ;
		try
		{
			Log.setupDefaultConfig() ;

			Config config = Server.parseOptions(args) ;
			System.out.println(config) ;

			XStreamPersistenceFactory xstreamPersistenceProvider = null ;
			HibernatePersistenceFactory hibernatePersistenceProvider = null ;
			PersistenceFactory persistenceProvider = null ;

			switch (config.persistence)
			{
			case HIBERNATE:
				hibernatePersistenceProvider = new HibernatePersistenceFactory(!config.fresh) ;
				persistenceProvider = hibernatePersistenceProvider ;
				break ;
			case XSTREAM:
				xstreamPersistenceProvider = new XStreamPersistenceFactory(!config.fresh, Definitions.getXmlFiles()) ;
				persistenceProvider = xstreamPersistenceProvider ;
				break ;
			}

			final ForumData data = new ForumData(persistenceProvider) ;

			OriginSearchEngineFactory originSearchProvider = null ;
			CompassSearchEngineFactory compassSearchProvider = null ;
			SearchEngineFactory searchProvider = null ;

			switch (config.searchEngine)
			{
			case ORIGIN:
				originSearchProvider = new OriginSearchEngineFactory(Definitions.INDEX_URI, data) ;
				searchProvider = originSearchProvider ;
				break ;
			case COMPASS:
				if (hibernatePersistenceProvider == null)
				{
					throw new Exception("compass needs hibernate") ;
				}
				compassSearchProvider = new CompassSearchEngineFactory(hibernatePersistenceProvider, data) ;
				searchProvider = compassSearchProvider ;
				break ;
			}


			final LocalForum theForum = new LocalForum(data, searchProvider) ;
			server = new Server("localhost", 2002, "forumserver", theForum) ;


			Webserver webServer = new Webserver(theForum) ;
			webServer.start() ;


			Log.getLogger(Subsystem.COMMUNICATION).config("server is up.") ;
			Server.waitForExitCommand() ;
		}
		catch (Exception e)
		{
			e.printStackTrace() ;
			Log.getLogger(Subsystem.COMMUNICATION).severe(e.getMessage()) ;
		}
		finally
		{
			if (server != null)
			{
				try
				{
					server.shutdown() ;
				}
				catch (IOException e)
				{
					Log.getLogger(Subsystem.COMMUNICATION).severe(e.getMessage()) ;
				}
			}
			Log.getLogger(Subsystem.COMMUNICATION).severe("exiting") ;
			System.exit(1) ;
		}

	}

	/**
	 * @param args command line argument to parse.
	 * @return configuration from command line options.
	 * @pre options are properly formatted.
	 */
	private static Config parseOptions(final String[] args)
	{
		Config config = new Config() ;
		config.fresh = false ;
		config.persistence = Config.PersistenceComponent.HIBERNATE ;
		config.searchEngine = Config.SearchEngineComponent.COMPASS ;

		for (String arg: args)
		{
			if (arg.equals("-f"))
			{
				config.fresh = true ;
			}
			else if (arg.startsWith("-p="))
			{
				String persistence = arg.split("=")[1].toUpperCase() ;
				config.persistence = Config.PersistenceComponent.valueOf(persistence) ;
			}
			else if (arg.startsWith("-s="))
			{
				String searchEngine = arg.split("=")[1].toUpperCase() ;
				config.searchEngine = Config.SearchEngineComponent.valueOf(searchEngine) ;
			}

		}

		return config ;
	}

	private static void setupDns(final int port) throws IOException
	{
		Log.getLogger(Subsystem.COMMUNICATION).config("setting up dns") ;
		if (Server.START_DNS_EXTERNALLY)
		{
			final String[] dnsCommand =
			{ Server.DNS_EXECUTION_COMMAND, String.valueOf(port) } ;
			Server.dns = Runtime.getRuntime().exec(dnsCommand) ;
		}
		else
		{
			LocateRegistry.createRegistry(port) ;
		}
	}

	private static void shutdownDns()
	{
		Log.getLogger(Subsystem.COMMUNICATION).config("shutting down dns") ;

		if (Server.dns != null)
		{
			Server.dns.destroy() ;
		}
	}

	private static void waitForExitCommand() throws IOException
	{
		new BufferedReader(new InputStreamReader(System.in)).readLine() ;
	}

	/**
	 * @param dnsAddress address for the naming server.
	 * @param dnsPort port on which the naming server resides.
	 * @param resourceName URL to register for this server.
	 * @param forum forum object to register as the forum.
	 * @throws IOException if there is a problem setting up the DNS or registering the server to public
	 * domain.
	 * @pre 'url' is a legal URL.
	 * @post the server is up.
	 */
	public Server(final String dnsAddress, final int dnsPort, final String resourceName,
		final LocalForum forum) throws IOException
	{
		this.resourceName = resourceName ;
		this.forum = forum ;
		this.dnsAddress = dnsAddress ;
		this.dnsPort = dnsPort ;

		Server.setupDns(dnsPort) ;
		this.registerServerToDns() ;
	}

	/**
	 * shutdown the server, including the DNS.
	 * @throws IOException if there is a problem unregistering the server from public domain.
	 */
	public void shutdown() throws IOException
	{
		this.forum.exit() ;
		try
		{
			this.unregisterServerFromDns() ;
		}
		finally
		{
			Server.shutdownDns() ;
			System.exit(0) ;
		}
	}

	private String getDnsUrl()
	{
		return "//" + this.dnsAddress + ":" + this.dnsPort + "/" + this.resourceName ;
	}

	private void registerServerToDns() throws MalformedURLException, IOException
	{
		for (int retries = 1 ; retries <= Server.MAX_RETRIES + 1 ; retries++)
		{
			try
			{
				final String url = this.getDnsUrl() ;
				Log.getLogger(Subsystem.COMMUNICATION).config("binding resource name") ;
				Naming.bind(url, this.forum) ;
				return ;
			}
			catch (final RemoteException e)
			{
				/*
				 * this could mean that the RMI registry is not ready yet. sleep and retry.
				 */
//				e.printStackTrace();
				if (retries <= Server.MAX_RETRIES)
				{
					try
					{
						Thread.sleep(500) ;
					}
					catch (final InterruptedException e1)
					{
						throw e ; // not e1
					}
				}
				else
				{
					throw e ;
				}
			}
			catch (final AlreadyBoundException e)
			{
				throw new IOException(e) ;
			}
		}
	}

	private void unregisterServerFromDns() throws RemoteException, MalformedURLException
	{
		try
		{
			final String url = this.getDnsUrl() ;
			Naming.unbind(url) ;
		}
		catch (final NotBoundException e)
		{
			throw new Bug("server was not set up.", e) ;
		}
	}
}
